#!/usr/bin/env python3
"""
Diff Patch Processor for Fortran Code Commentator

This module handles git diff patch validation and application with comprehensive error handling.

Author: GSI Development Team
Date: 2025-01-XX
"""

import os
import re
import subprocess
import tempfile
import shutil
import hashlib
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any

from ..core.models import ValidationResult, ApplicationResult, BackupInfo

logger = logging.getLogger(__name__)


class DiffPatchProcessor:
    """Handles git diff patch validation and application with comprehensive error handling"""
    
    def __init__(self, backup_dir: Optional[str] = None):
        # Enhanced pattern matching for diff validation
        self.diff_header_pattern = re.compile(r'^--- a/(.+)$', re.MULTILINE)
        self.plus_header_pattern = re.compile(r'^\+\+\+ b/(.+)$', re.MULTILINE)
        self.hunk_header_pattern = re.compile(r'^@@ -(\d+),(\d+) \+(\d+),(\d+) @@(.*)$', re.MULTILINE)
        self.diff_line_pattern = re.compile(r'^([ +-])(.*)$', re.MULTILINE)
        
        # Setup backup directory
        self.backup_dir = Path(backup_dir) if backup_dir else None
        if self.backup_dir:
            self.backup_dir.mkdir(parents=True, exist_ok=True)
            logger.info(f"Backup directory initialized: {self.backup_dir}")
    
    def validate_diff_format(self, diff_content: str) -> ValidationResult:
        """Comprehensive unified diff format validation"""
        errors = []
        warnings = []
        suggested_fixes = []
        
        if not diff_content.strip():
            errors.append("Empty diff content")
            return ValidationResult(False, errors, warnings, suggested_fixes)
        
        lines = diff_content.split('\n')
        
        # Check for required headers
        if not any(line.startswith('--- a/') for line in lines):
            errors.append("Missing '--- a/filename' header")
            
        if not any(line.startswith('+++ b/') for line in lines):
            errors.append("Missing '+++ b/filename' header")
            
        # Check for hunk headers
        if not any(line.startswith('@@') for line in lines):
            errors.append("No valid hunk headers found")
            suggested_fixes.append("Add '@@ -old_start,old_count +new_start,new_count @@' hunk headers")
        
        # Validate line prefixes in hunks
        in_hunk = False
        for i, line in enumerate(lines):
            if line.startswith('@@'):
                in_hunk = True
                continue
            
            if in_hunk and line:
                prefix = line[0]
                if prefix not in [' ', '+', '-']:
                    errors.append(f"Invalid line prefix '{prefix}' at line {i+1}")
        
        is_valid = len(errors) == 0
        return ValidationResult(is_valid, errors, warnings, suggested_fixes)
    
    def apply_patch(self, diff_content: str, target_file: str) -> ApplicationResult:
        """Apply diff patch with comprehensive error handling and backup"""
        target_path = Path(target_file)
        
        try:
            # Create backup
            backup_info = self._create_enhanced_backup(target_file)
            logger.debug(f"Created backup: {backup_info.backup_path}")
            
            # Clean the diff content before applying
            cleaned_diff = self._clean_patch_content(diff_content)
            
            # Try direct patch application first (more reliable than git apply)
            direct_result = self._apply_patch_directly(cleaned_diff, target_file)
            if direct_result:
                result = subprocess.CompletedProcess(['direct_apply'], 0, '', '')
            else:
                # Fallback to git apply with more lenient settings
                result = subprocess.run(
                    ['git', 'apply', '--verbose', '--whitespace=fix', '--ignore-whitespace', '-'],
                    input=cleaned_diff,
                    text=True,
                    capture_output=True,
                    cwd=target_path.parent,
                    timeout=60
                )
            
            if result.returncode == 0:
                # Validate Fortran syntax
                syntax_valid = self.validate_fortran_syntax(target_file)
                
                return ApplicationResult(
                    success=True,
                    backup_info=backup_info,
                    syntax_validation_passed=syntax_valid
                )
            else:
                # Restore from backup on failure
                self._restore_from_backup(backup_info)
                
                return ApplicationResult(
                    success=False,
                    error_message=result.stderr,
                    backup_info=backup_info
                )
                
        except Exception as e:
            # Restore from backup on any exception
            if 'backup_info' in locals():
                self._restore_from_backup(backup_info)
            
            return ApplicationResult(
                success=False,
                error_message=str(e)
            )
    
    def _create_enhanced_backup(self, file_path: str) -> BackupInfo:
        """Create timestamped backup with metadata"""
        import time
        from datetime import datetime
        
        source_path = Path(file_path)
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        
        if self.backup_dir:
            backup_path = self.backup_dir / f"{source_path.name}.{timestamp}.backup"
        else:
            backup_path = source_path.with_suffix(f"{source_path.suffix}.{timestamp}.backup")
        
        # Copy file with metadata
        shutil.copy2(source_path, backup_path)
        
        # Calculate checksum
        with open(source_path, 'rb') as f:
            checksum = hashlib.md5(f.read()).hexdigest()
        
        # Get file info
        stat_info = source_path.stat()
        
        return BackupInfo(
            original_path=str(source_path),
            backup_path=str(backup_path),
            timestamp=timestamp,
            checksum=checksum,
            file_size=stat_info.st_size
        )
    
    def _restore_from_backup(self, backup_info: BackupInfo) -> bool:
        """Restore file from backup"""
        try:
            shutil.copy2(backup_info.backup_path, backup_info.original_path)
            logger.info(f"Restored {backup_info.original_path} from backup")
            return True
        except Exception as e:
            logger.error(f"Failed to restore from backup: {e}")
            return False
    
    def validate_fortran_syntax(self, file_path: str) -> bool:
        """Basic Fortran syntax validation"""
        try:
            with open(file_path, 'r') as f:
                content = f.read()
            
            lines = content.split('\n')
            
            # Check for basic Fortran constructs
            subroutine_count = 0
            function_count = 0
            module_count = 0
            end_count = 0
            
            for line in lines:
                line_lower = line.strip().lower()
                
                if line_lower.startswith('subroutine '):
                    subroutine_count += 1
                elif line_lower.startswith('function '):
                    function_count += 1
                elif line_lower.startswith('module '):
                    module_count += 1
                elif line_lower.startswith('end ') or line_lower == 'end':
                    end_count += 1
            
            # Basic validation - should have matching constructs
            total_constructs = subroutine_count + function_count + module_count
            
            # Allow some flexibility in end statement matching
            if total_constructs > 0 and end_count == 0:
                logger.warning(f"No END statements found for {total_constructs} constructs")
                return False
            
            return True
            
        except Exception as e:
            logger.error(f"Error validating Fortran syntax: {e}")
            return False
    
    def _clean_patch_content(self, diff_content: str) -> str:
        """Clean patch content to improve git apply compatibility"""
        lines = diff_content.split('\n')
        cleaned_lines = []
        
        for line in lines:
            # Remove trailing whitespace from all lines
            cleaned_line = line.rstrip()
            
            # Handle special cases for diff format
            if line.startswith('--- ') or line.startswith('+++ '):
                # Ensure proper diff headers
                cleaned_lines.append(cleaned_line)
            elif line.startswith('@@ '):
                # Ensure proper hunk headers
                cleaned_lines.append(cleaned_line)
            elif line.startswith(('+', '-', ' ')):
                # Content lines - preserve the prefix but clean trailing space
                if len(line) > 0:
                    prefix = line[0]
                    content = line[1:].rstrip() if len(line) > 1 else ''
                    cleaned_lines.append(prefix + content)
                else:
                    cleaned_lines.append(line)
            else:
                # Other lines - clean but preserve
                cleaned_lines.append(cleaned_line)
        
        # Remove completely empty lines at the end
        while cleaned_lines and not cleaned_lines[-1].strip():
            cleaned_lines.pop()
        
        return '\n'.join(cleaned_lines)
    
    def _apply_patch_directly(self, diff_content: str, target_file: str) -> bool:
        """Apply patch by directly editing the file (more reliable than git apply)"""
        try:
            # Read the current file
            with open(target_file, 'r', encoding='utf-8') as f:
                original_lines = f.readlines()
            
            # Parse the diff to extract additions
            additions = self._extract_additions_from_diff(diff_content)
            if not additions:
                return False
            
            # Apply additions by inserting at appropriate locations
            modified_lines = original_lines.copy()
            
            # Sort additions by line number in reverse order to avoid offset issues
            for line_num, new_lines in sorted(additions.items(), reverse=True):
                # Insert new lines at the specified position
                insert_pos = line_num - 1  # Convert to 0-based index
                if 0 <= insert_pos <= len(modified_lines):
                    for i, line in enumerate(new_lines):
                        modified_lines.insert(insert_pos + i, line + '\n')
            
            # Write the modified content back
            with open(target_file, 'w', encoding='utf-8') as f:
                f.writelines(modified_lines)
            
            logger.info(f"Successfully applied patch directly to {target_file}")
            return True
            
        except Exception as e:
            logger.warning(f"Direct patch application failed: {e}")
            return False
    
    def _extract_additions_from_diff(self, diff_content: str) -> dict:
        """Extract added lines from diff content"""
        additions = {}  # line_number -> [list of lines to add]
        lines = diff_content.split('\n')
        
        current_line = None
        in_hunk = False
        
        for line in lines:
            if line.startswith('@@ '):
                # Parse hunk header to get starting line number
                # Format: @@ -old_start,old_count +new_start,new_count @@
                import re
                match = re.match(r'@@ -\d+,\d+ \+(\d+),\d+ @@', line)
                if match:
                    current_line = int(match.group(1))
                    in_hunk = True
                continue
                
            if in_hunk and line.startswith('+') and not line.startswith('+++'):
                # This is an added line
                added_content = line[1:]  # Remove the '+' prefix
                if current_line not in additions:
                    additions[current_line] = []
                additions[current_line].append(added_content)
                # Don't increment current_line for added lines - they stay at same position
            elif in_hunk and line.startswith(' '):
                # Context line, increment line number
                current_line += 1
        
        return additions